/** @class tServ
 tServ is granular, queue based application server

 to enable core dumps: 
 sudo echo "1" > /proc/sys/kernel/core_uses_pid
 ulimit -c unlimited

 $Id: tServ.cpp 2 2012-04-04 15:07:32Z gerhardus1@gmail.com $
 Versioning: a.b.c a is a major release, b represents changes or new features, c represents bug fixes. 
 @version 1.0.0   06/10/2011    Gerhardus Muller     Script created

 @note

 @todo
 
 @bug

 Copyright Gerhardus Muller
 */

#include <fstream>
#include <iostream>
#include <sstream>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h> 
#include <sys/wait.h>
#include <pwd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/prctl.h>

#include "../src/tServ.h"
//#include "networkIf/networkIf.h"
//#include "nucleus/nucleus.h"
//#include "../src/recoveryLog.h"
#include "../utils/utils.h"
#include "../utils/epoll.h"

bool tServ::bRunning = true;
bool tServ::bChildSignal = false;
int  tServ::signalFd0 = 0;
options* pOptions = NULL;
tServ* theServer = NULL;

/**
 Construction
 */
tServ::tServ( int theArgc, char* theArgv[] )
  : object( "tServ" )
{
  memset( signalFd, 0, sizeof( signalFd ) );
  pEpollRd = NULL;
  pEpollWr = NULL;
  pRecSock = NULL;
  pSignalSock = NULL;
  //pRecoveryLog = NULL;
  pid = getpid( );
  argc = theArgc;
  argv = theArgv;
}	// tServ

/**
 * deletes objects
 * **/
void tServ::cleanupObj( )
{
  if( pEpollRd != NULL ) delete pEpollRd;
  if( pEpollWr != NULL ) delete pEpollWr;
  if( pRecSock != NULL )    {delete pRecSock; pRecSock=NULL;};
  if( pSignalSock != NULL ) {delete pSignalSock; pSignalSock=NULL;};
  //if( pRecoveryLog != NULL ){delete pRecoveryLog; pRecoveryLog=NULL;};
} // cleanupObj

/**
 Destruction
 */
tServ::~tServ()
{
  log.info( log.LOGNORMAL, "tServ::~tServ" );
  waitForChildrenToExit( );
  cleanupObj();

  if( signalFd[0] != 0 ) close( signalFd[0] );
  if( signalFd[1] != 0 ) close( signalFd[1] );
}	// ~tServ

/**
 Standard logging call - produces a generic text version of the tServ.
 Memory allocation / deleting is handled by this tServ.
 @return pointer to a string describing the state of the tServ.  The string has an unspecified lifetime and is not multi-thread safe
 */
std::string tServ::toString( )
{
  std::ostringstream oss;
  oss << typeid(*this).name() << " pid " << getpid( );
	return oss.str();
}	// toString

/**
 * init - creates networkIf and required functions
 * **/
void tServ::init( )
{
  log.info( log.LOGALWAYS ) << "init: tServ - build no " << buildno << " build time " << buildtime;
  unixSocket::createSocketPair( signalFd, "signalFd" );
  log.info( log.LOGALWAYS, "init: tServ signalFd %d %d", signalFd[0], signalFd[1] );
//  pRecoveryLog = new recoveryLog( pOptions->logBaseDir.c_str(), false, pOptions->logrotatePath, pOptions->runAsUser, pOptions->logGroup, pOptions->logFilesToKeep ); // do not rotate here as well
//  tMessage::theRecoveryLog = pRecoveryLog;
  
  // create the polling instance
  pEpollRd = new epoll( 10 );
  pEpollWr = new epoll( 10 );
  pEpollRd->setLoggingId( "pEpollRd" );
  pEpollWr->setLoggingId( "pEpollWr" );

  // create the standard pipes and populate the poll object
  signalFd0 = signalFd[0];
  pSignalSock = new unixSocket( signalFd[1], false, "signalFd1" );
  pEpollRd->addFd( signalFd[1], (void*)signalFd[1], EPOLLIN );
//  pSignalSock->setNonblocking( );
//  pRecSock = new unixSocket( mainFd[1], false, "mainFd1" );
//  pRecSock->setNonblocking( );
//  pRecSock->initPoll( 2 );
//  pRecSock->addReadFd( mainFd[1] );
  
  // retrieve our hostname
  char hostname[256];
  hostname[0] = '\0';
  gethostname( hostname, 255 );
  hostId = hostname;

  // seed the random number generator
  srandom( time(NULL) );

  // drop executing rights to a non-priviledged user
  bool bResult = dropPriviledge( pOptions->runAsUser.c_str() );
  if( !bResult )
    throw Exception( log, log.ERROR, "init: failed to drop priviledges to user %s\n", pOptions->runAsUser.c_str() );
} // init

/**
 * run as a daemon
 * **/
void tServ::runAsDaemon( )
{
  int fd0, fd1, fd2;
  struct rlimit rl;
  
  getrlimit( RLIMIT_NOFILE, &rl );

  // become a session leader to loose the controlling TTY
  if( ( pid = fork() ) < 0 )
  {
    fprintf( stderr, "runAsDaemon: failed to fork\n" );
    exit( 1 );
  }
  else if( pid != 0 )
  {
    // parent logs the pid so that logrotate and others can find it, then exits
    log.info( log.LOGALWAYS, "runAsDaemon: PID %d", pid );
    FILE* pidFile = fopen( pOptions->pidName.c_str(), "w" );
    if( pidFile == NULL )
      log.warn( log.LOGALWAYS, "runAsDaemon: failed to write pid to %s", pOptions->pidName.c_str() );
    else
    {
      fprintf( pidFile, "%d", pid );
      fclose( pidFile );
    }
    exit( 0 );
  } // else parent
  setsid( );

  // close the open standard file handle - cannot close all as assumed
  // the diva sdk has already opened its log
  //  if( rl.rlim_max == RLIM_INFINITY )
  //    rl.rlim_max = 1024;
  rl.rlim_max = 3;
  for( unsigned int i = 0; i < rl.rlim_max; i++ )
    close( i );
  
  // attach file descriptors 0,1,2 to /dev/null
  fd0 = open( "/dev/null", O_RDWR );
  fd1 = dup( 0 );
  fd2 = dup( 0 );
  if( (fd0 != 0) || (fd1 != 1) || (fd2 != 2) )
    log.warn( log.LOGALWAYS, "runAsDaemon: funny file descriptors %d, %d, %d", fd0, fd1, fd2 );

  pid = getpid( );
} // runAsDaemon

/**
 * main tServ processing loop
 * **/
void tServ::main( )
{
  unsigned int termTime = 0;
  now = time( NULL );
  if( pOptions->maintInterval > 0 )
    alarm( pOptions->maintInterval );
  else
    log.warn( log.LOGALWAYS, "main: not running a maintenance timer" );
  nextExpiredEventCheck = now + pOptions->expiredEventInterval;

  try
  {
    init( );

    // main loop
    int numReady = 0;
    while( bRunning )
    {
      log.debug( log.LOGNORMAL, "main: bRunning %d", bRunning );
      try
      {
        numReady = 0;
        numReady = pEpollRd->waitForEvent(-1);
      }
      catch( Exception e )
      {
        log.warn( log.LOGALWAYS, "main: exception from waitForEvent" );
        bRunning = false;   // fatal error
        throw;
      } // catch

      now = time( NULL );
      if( numReady > 0 )
      {
        try
        {
          for( int i=0; i<numReady; i++ )
          {
          } // for
        } // try
        catch( Exception e )
        {
          // dump epoll object and rebuild
          // rebuildPollObjects();
        } // catch

#if 0
        int fd = 0;
        // retrieve the available file descriptors and process the events
        while( ( fd = pRecSock->getNextFd( ) ) > 0 )
        {
          log.debug( log.LOGNORMAL, "main: fd %d has data", fd );

          // service an event from the outside or a signal generated request
          try
          {
            if( fd == eventSourceFd )
            {
              tMessage* pEvent = NULL;
              pEvent = tMessage::unSerialise( pRecSock );

              while( pEvent != NULL )
              {
                if( pEvent->getType() == tMessage::EV_COMMAND )
                {
                  if( pEvent->getCommand( ) == tMessage::CMD_STATS ) 
                  { // request to dump stats after receiving a SIGHUP
                    log.info( log.LOGMOSTLY, "main: received a CMD_STATS" );
                    std::string time = utils::timeToString( now );
                    tMessage* pChildCommand = new tMessage( tMessage::EV_COMMAND );
                    pChildCommand->setCommand( tMessage::CMD_STATS );
                    pChildCommand->addParam( "time", time );
                    if( pOptions->statsChildrenAddress.length() > 0 )
                      notifySlaveStatsServers( pChildCommand );
                    sendCommandToChildren( pChildCommand );
                    delete pChildCommand;

                    dumpHttp( time );
                  } // if CMD_STATS
                  else if( pEvent->getCommand( ) == tMessage::CMD_TIMER_SIGNAL )
                  { // the alarm is used to generate stats regularly
                    alarm( pOptions->statsInterval );
                    log.info( log.LOGNORMAL, "main: received a CMD_TIMER_SIGNAL" );
                    time_t now1 = now;
                    struct tm* t = localtime( &now1 );
                    log.debug( log.LOGONOCCASION, "main: bStatsHttp hour %d start %d stop %d", t->tm_hour, pOptions->statsHourStart, pOptions->statsHourStop );
                    if( (t->tm_hour >= pOptions->statsHourStart) && (t->tm_hour < pOptions->statsHourStop) )
                    {
                      std::string time = utils::timeToString( now );
                      tMessage* pChildCommand = new tMessage( tMessage::EV_COMMAND );
                      pChildCommand->setCommand( tMessage::CMD_STATS );
                      pChildCommand->addParam( "time", time );
                      if( pOptions->statsChildrenAddress.length() > 0 )
                        notifySlaveStatsServers( pChildCommand );
                      sendCommandToChildren( pChildCommand );
                      delete pChildCommand;

                      dumpHttp( time );
                    } // if
                  } // if CMD_TIMER_SIGNAL
                  else if( pEvent->getCommand( ) == tMessage::CMD_CHILD_SIGNAL ) 
                  {
                    log.warn( log.LOGALWAYS,"main: received a CMD_CHILD_SIGNAL" );
                    handleChildDone( );
                  } // if CMD_CHILD_SIGNAL
                  else if( pEvent->getCommand( ) == tMessage::CMD_REOPEN_LOG ) 
                  {
                    log.info( log.LOGMOSTLY, "main: received a CMD_REOPEN_LOG" );
                    sendCommandToChildren( tMessage::CMD_REOPEN_LOG );
                    log.reopenLogfile( );
                    pRecoveryLog->reOpen( );
                  } // if CMD_REOPEN_LOG
                  else if( pEvent->getCommand( ) == tMessage::CMD_RESET_STATS ) 
                  {
                    log.info( log.LOGMOSTLY, "main: received a CMD_RESET_STATS" );
                    sendCommandToChildren( tMessage::CMD_RESET_STATS );
                  } // if( CMD_RESET_STATS
                  else if( (pEvent->getCommand( ) == tMessage::CMD_SHUTDOWN) || (pEvent->getCommand( ) == tMessage::CMD_EXIT_WHEN_DONE) )
                  {
                    log.info( log.LOGALWAYS, "main: received a %s", pEvent->commandToString() );
                    // shutdown the controller and networkIf - allow the nucleus to run to service the 
                    // remainder of the controller events
                    // TODO: the correct behaviour would be to only prevent new connections from the 
                    // outside as we may have establised TCP connections waiting for responses
                    if( termTime == 0 )
                    {
                      termTime = now;
                      bAutoFork = false;
                    } // if
                    if( networkIfPid > 0 ) sendCommandToChild( tMessage::CMD_SHUTDOWN, networkIfFd[0] );
                    if( nucleusPid > 0 ) sendCommandToChild( tMessage::CMD_EXIT_WHEN_DONE, nucleusFd[0] );
                  } // if( CMD_SHUTDOWN
                  else if( pEvent->getCommand( ) == tMessage::CMD_MAIN_CONF ) 
                  {
                    reconfigure( pEvent );
                  } // if( CMD_MAIN_CONF
                  else
                    log.warn( log.LOGALWAYS,"main: tMessage failed to handle command %d", pEvent->getCommand( ) );
                } // if tMessage::EV_COMMAND
                else
                  log.error( "main: unable to handle event %s", typeid(*pEvent).name() );

                log.debug( log.LOGNORMAL, "main: done with event processing 1" );
                delete pEvent;
                pEvent = NULL;
                pEvent = tMessage::unSerialise( pRecSock );
              } // while
            } // if( fd == eventSouceFd )
            else if( fd == signalFd[1] )  // service a signal event - repost as normal events
            {
              tMessage::eCommandType theCommand = tMessage::CMD_NONE;
              int bytesReceived = pSignalSock->read( (char*)(&theCommand), sizeof(theCommand) );
              while( bytesReceived == sizeof(theCommand) )
              {
                tMessage* pEvent = new tMessage( tMessage::EV_COMMAND );
                pEvent->setCommand( theCommand );
                log.info( log.LOGMOSTLY, "main: redispatching signal command '%s'", pEvent->toString().c_str() );
                pEvent->serialise( mainFd[0] );
                delete pEvent;

                bytesReceived = pSignalSock->read( (char*)(&theCommand), sizeof(theCommand) );
              } // while
            } // if( fd == signalFd[1]
          } // try
          catch( Exception e )
          {
            log.warn( log.LOGALWAYS, "main: ignoring exception in main loop" );
          } // catch
          catch( std::runtime_error e )
          { // json-cpp throws runtime_error
            log.error( "main: ignoring std::runtime_error:'%s'", e.what() );
          } // catch
        } // while fd = pRecSock->getNextFd
#endif
      } // if( numReady > 0

      // signal all children to exit
      if( termTime > 0 )
      {
      } // if
    } // while bRunning
  } // try
  catch( Exception e )
  {
    // exception has been logged - exit
    log.warn( log.LOGALWAYS, "main: ignoring 1 exception in main" );
    bRunning = false;
  } // catch
} // main

/**
 * handles a reconfigure command
 * TODO - most likely needs to be propagated to the children
 * **/
void tServ::reconfigure( tMessage* pCommand )
{
  try
  {
    log.info( log.LOGMOSTLY ) << "reconfigure: received command '" << pCommand->toString() << "'";
    std::string cmd = pCommand->getParam( "cmd" );
    if( (cmd.length()==0) )
      throw Exception( log, log.WARN, "reconfigure: require parameter cmd" );

    if( cmd.compare( "loglevel" ) == 0 )
    {
      std::string val = pCommand->getParam( "val" );
      if( val.length()==0 )
        throw Exception( log, log.WARN, "reconfigure: require parameter val for command '%s'", cmd.c_str() );

      int newLevel = atoi( val.c_str() );
      log.info( log.LOGALWAYS, "reconfigure: setting loglevel to %d", newLevel );
      pOptions->defaultLogLevel = newLevel;
      log.setDefaultLevel( (loggerDefs::eLogLevel) pOptions->defaultLogLevel );
    } // if( cmd.compare
    else
      log.warn( log.LOGALWAYS, "reconfigure: cmd '$s' not supported", cmd.c_str() );
  } // try
  catch( Exception e )
  {
  } // catch
} // reconfigure

/**
 * sends a kill to all children
 * TODO - signal what used to be the nucleus children
 * **/
void tServ::termChildren( )
{
  log.info( log.LOGALWAYS, "termChildren" );
} // termChildren

/**
 * sends command requests to the children
 * @param command - the command to send
 * @param fd - the file descriptor for the child
 * TODO - signal what used to be the nucleus children
 * **/
//void tServ::sendCommandToChild( tMessage::eCommandType command, int fd )
//{
//} // sendCommandToChild

/**
 * sends command requests to the children
 * @param command - the command to send
 * TODO - signal what used to be the nucleus children
 * **/
//void tServ::sendCommandToChildren( tMessage::eCommandType command )
//{
//} // sendCommandToChildren

/**
 * sends command requests to the children
 * @param pCommand - the command to send
 * TODO - signal what used to be the nucleus children
 * **/
//void tServ::sendCommandToChildren( tMessage* pCommand )
//{
//} // sendCommandToChildren

/**
 * signal children
 * @param sig - the signal to send
 * TODO - signal what used to be the nucleus children
 * **/
//void tServ::signalChildren( int sig )
//{
//  log.debug( log.LOGNORMAL, "signalChildren sig %s", strsignal(sig) );
//} // signalChildren

/**
 * handle a child that exited
 * TODO - handle what used to be the nucleus children
 * **/
bool tServ::handleChildDone( )
{
//  int status;
//  int childPid = -1;
  bool bChildExited = false;
  
  log.debug( log.LOGNORMAL, "handleChildDone:" );
  return bChildExited;
} // handleChildDone

/**
 * wait for children to exit
 * TODO - catch what used to be the nucleus children
 * **/
void tServ::waitForChildrenToExit( )
{
  log.info( log.LOGALWAYS, "waitForChildrenToExit:" );

  // wait for the children to exit - eventually wait returns ECHILD - no children
  int pid = 0;
  int status = 0;
  pid = wait( &status );
  while( ( pid != -1 ) || (( pid==-1)&&(errno==EINTR)) )
  {
    if( pid > 0 )
    {
      log.warn( log.LOGALWAYS, "waitForChildrenToExit: unknown pid %d", pid );
    } // if
    pid = wait( &status );
  } // while
} // waitForChildrenToExit

/**
 * drops priviledges from root - drop only temporarily to allow child processes to regain
 * if so configured
 * @param user
 * @return true on success
 * **/
bool tServ::dropPriviledge( const char* user )
{
  if( getuid() != 0 )
  {
    log.warn( log.LOGALWAYS, "dropPriviledge: not running as root, cannot drop priviledge" );
    return true;
  } // if

  struct passwd* pw = getpwnam( user );
  if( pw == NULL ) 
  {
    log.error( "dropPriviledge: could not find user %s", user );
    return false;
  }
  int userId = pw->pw_uid;
  int groupId = pw->pw_gid; 
  log.info( log.LOGALWAYS, "dropPriviledge: to user %s uid %d gid %d pid %d ruid:%d, euid:%d", user, userId, groupId, getpid(), getuid(), geteuid() );
  
  int result = setgid( groupId );
  if( result == -1 )
  {
    log.error( "dropPriviledge: failed on setgid - %s", strerror(errno) );
    return false;
  }
  result = utils::dropPriviledgeTemp( userId );
  if( result != 0 )
  {
    log.error( "dropPriviledge: failed on setuid - %s", strerror(errno) );
    return false;
  }
  log.info( log.LOGALWAYS, "dropPriviledge: after drop ruid:%d, euid:%d", getuid(), geteuid() );
  result = utils::restorePriviledge();
  log.debug( log.LOGALWAYS, "dropPriviledge: after restore ruid:%d, euid:%d", getuid(), geteuid() );
  result = utils::dropPriviledgeTemp( userId );
  log.debug( log.LOGALWAYS, "dropPriviledge: after drop ruid:%d, euid:%d", getuid(), geteuid() );

  // set the process to be dumpable
  if( prctl( PR_SET_DUMPABLE, 1, 0, 0, 0 ) == -1 )
    log.error( "dropPriviledge: prctl(PR_SET_DUMPABLE) error - %s", strerror(errno) );

  return true;
} // dropPriviledge

/**
 * sends command to main loop from receiving a signal
 * use a static signal event to prevent signal safe functions in the construction
 * of a new event
 * @param command - the command to send
 * **/
void tServ::sendSignalCommand( tMessage::eCommandType command )
{
  int ret = write( signalFd0, (const void*)&command, sizeof(command) );
  if( ret != sizeof(command) ) fprintf( stderr, "tServ::sendSignalCommand failed to write command %d to fd %d\n", command, signalFd0 );
} // sendSignalCommand

/**
 * signal handler
 * @param signo
 * **/
void tServ::sigHandler( int signo )
{
  switch( signo )
  {
    case SIGINT:
      sendSignalCommand( tMessage::CMD_SHUTDOWN );
      break;
    case SIGTERM:
      sendSignalCommand( tMessage::CMD_SHUTDOWN );
      break;
    case SIGCHLD:
      sendSignalCommand( tMessage::CMD_CHILD_SIGNAL );
      break;
    case SIGUSR1:
      sendSignalCommand( tMessage::CMD_RESET_STATS );
      break;
    case SIGUSR2:
      sendSignalCommand( tMessage::CMD_REOPEN_LOG );
      break;
    case SIGHUP:
      sendSignalCommand( tMessage::CMD_STATS );
      break;
    case SIGALRM:
      sendSignalCommand( tMessage::CMD_TIMER_SIGNAL );
      bChildSignal = true;  // used in recovery
      break;
    default:
      sendSignalCommand( tMessage::CMD_NONE );
  } // switch
} // sigHandler

/**
 * App main
 * @param argc
 * @param argv
 * **/
int main( int argc, char* argv[] )
{
  pOptions = new options( );
  bool bDone = !pOptions->parseOptions( argc, argv );
  if( bDone )   // typically the user requested version or help
  {
    delete pOptions;
    return 0;
  } //  if

  try
  {
    theServer = new tServ( argc, argv );

    // init logging
    theServer->log.openLogfile( pOptions->logFile.c_str(), pOptions->bFlushLogs );
    theServer->log.setDefaultLevel( (loggerDefs::eLogLevel)pOptions->defaultLogLevel );
    if( pOptions->bLogStderr )
      theServer->log.setLogStdErr( true );
    else if( pOptions->bLogConsole )
      theServer->log.setLogConsole( true );
    else
      theServer->log.setLogConsole( false );
    theServer->log.setAddPid( true );
    theServer->log.setAddExecTrace( true );
    theServer->log.generateTimestamp();
//    unixSocket::pStaticLogger->init( "unixSocket", (loggerDefs::eLogLevel)pOptionsNucleus->defaultLogLevel );
//    unixSocket::pStaticLogger->setAddPid( true );
    pOptions->logOptions(); 

    // daemonise if requested
    if( pOptions->bDaemon )
      theServer->runAsDaemon();
  } // try
  catch( Exception e )
  {
    delete pOptions;
    if( theServer != NULL ) delete theServer;
    return -1;
  } // catch

  // set the default umask
  umask( S_IWGRP | S_IXGRP | S_IWOTH | S_IXOTH );

  // register signal handling
  signal( SIGINT, tServ::sigHandler );
  signal( SIGTERM, tServ::sigHandler );
  signal( SIGCHLD, tServ::sigHandler );
  signal( SIGUSR1, tServ::sigHandler );
  signal( SIGUSR2, tServ::sigHandler );
  signal( SIGHUP, tServ::sigHandler );
  signal( SIGALRM, tServ::sigHandler );
  
  // execute the main process
  theServer->main( );

  // cleanup deletes the global objects
  delete theServer;
  delete pOptions;
  return 0;
} // main

